---
-- 工具类

local config = require("config")
local string_util = require("lib.string_util")

local _M = {}

local _find_file_in_dir
do
    local lfs = require("lfs")
    local moses = require("lib.packages.moses_min")

    ---查找目录下的文件
    ---@param path string 待查找目录路径(不需要最后一个"/")
    ---@param recursive nil|boolean true:递归查找所有子目录 (默认nil/false 只查找path一级目录)
    ---@param file_suffix nil|string 文件后缀(默认nil/空字符串 返回任意文件)
    ---@return table
    _find_file_in_dir = function(path, recursive, file_suffix)
        local result = {}
        for file in lfs.dir(path) do
            if file ~= "." and file ~= ".." then
                local file_path = path .. "/" .. file
                -- lfs.attributes() 可能的值:
                --      file、directory、link、socket、named pipe、char device、block device、other
                local file_attr = lfs.attributes(file_path)
                if file_attr.mode == "directory" then
                    -- 当前 file_path 是目录
                    if recursive then
                        local res = _find_file_in_dir(file_path, recursive, file_suffix)
                        if not moses.isEmpty(res) then
                            result = moses.union(result, res)
                        end
                    end
                else
                    -- 当前 file_path 是非目录
                    if type(file_suffix) == "string" and file_suffix ~= "" then
                        local res = string.find(file_path, file_suffix, -1 * string.len(file_suffix), true)
                        if res ~= nil then
                            table.insert(result, file_path)
                        end
                    else
                        table.insert(result, file_path)
                    end
                end
            end
        end
        return result
    end
    _M.find_file_in_dir = _find_file_in_dir
end

---检测开关是否为打开状态.
---<br>     如果参数为 "on"/"true"/"yes"/"y"/"1"/true/1 则为打开状态,返回true ;
---<br>     如果参数为 "off"/"false"/"no"/"n"/"0"/false/0 则为关闭状态,返回false ;
---<br>     其他返回 nil ;
---@param value string|boolean|number 待检测值(字符串不区分大小写)
---@return boolean|nil
_M.check_switch_is_on = function(value)
    local v = string.lower(tostring(value))
    if v == "on" or v == "true" or v == "yes" or v == "y" or v == "1" then
        return true
    elseif v == "off" or v == "false" or v == "no" or v == "n" or v == "0" then
        return false
    end
    return nil
end

---检测是否为有效IP(IPv4/IPv6都可)
---@param ip string 待检测IP
---@return boolean
_M.ip_valid = function(ip)
    local pattern =
    [[^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){6}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^::([\da-fA-F]{1,4}:){0,4}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:):([\da-fA-F]{1,4}:){0,3}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){2}:([\da-fA-F]{1,4}:){0,2}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){3}:([\da-fA-F]{1,4}:){0,1}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){4}:((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$|^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}$|^:((:[\da-fA-F]{1,4}){1,6}|:)$|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)$|^([\da-fA-F]{1,4}:){2}((:[\da-fA-F]{1,4}){1,4}|:)$|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)$|^([\da-fA-F]{1,4}:){4}((:[\da-fA-F]{1,4}){1,2}|:)$|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?$|^([\da-fA-F]{1,4}:){6}:$]]
    local from, _, _ = ngx.re.find(ip, pattern)
    if from then
        return true
    else
        return false
    end
end

---获取客户端IP。
---优先级为 HEADER["X-Real-Ip"] -> HEADER["X-Forwarded-For"] -> nginx $remote_addr -> "unknown"
---@return string
_M.get_client_ip = function()
    local headers = ngx.req.get_headers()
    local ip = headers["x_real_ip"] or headers["x_forwarded_for"] or ngx.var.remote_addr or "unknown"
    return ip
end

---获取客户端 user-agent，未知返回"unknown"。
---@return string
_M.get_user_agent = function()
    local ua = ngx.var.http_user_agent
    if ua == nil or ua == "" then
        ua = "unknown"
    end
    return ua
end

---记录WAF相关日志到 ngx.ctx.easy_ngx_waf_log 数组中,在log_by_lua阶段写入日志文件
---@param log_text string
_M.waf_log = function(log_text)
    if (not _M.check_switch_is_on(config.waf_log)) or log_text == nil or log_text == "" then
        return
    end
    -- 同一次请求 ip host request ua 等只记录一次, msg 可有多个
    if ngx.ctx.easy_ngx_waf_log == nil then
        ngx.ctx.easy_ngx_waf_log = {
            ip = _M.get_client_ip(),
            time = ngx.localtime(),
            host = ngx.var.host,
            req = ngx.var.request,
            ua = _M.get_user_agent(),
            msg = {}
        }
    end

    table.insert(ngx.ctx.easy_ngx_waf_log.msg, log_text)
end

---字节数字转为 B/KB/MB/GB/TB 可读形式字符串
---@param num number 待转换数值
---@param dicimal_len nil|number 保留小数位数(默认没有小数位)
---@return nil|string
_M.byte2human_str = function(num, dicimal_len)
    -- 1024  1KB
    -- 1048576  1MB
    -- 1073741824  1GB
    -- 1099511627776  1TB
    if type(num) ~= "number" then
        error("util.byte2human_str(num) excepted number.")
        return nil, "not number"
    end

    if num < 1024 then
        return num .. "B"
    end

    -- 默认保留0位小数位,运算因子为1
    local dicimal_n = 1
    if type(dicimal_len) == "number" and dicimal_len >= 1 then
        dicimal_n = math.pow(10, dicimal_len)
    end

    local unit = { "KB", "MB", "GB", "TB" }
    local res = num / 1024
    local count = 1
    while (res >= 1024 and count < 4)
    do
        res = res / 1024
        count = count + 1
    end
    -- 最后四舍五入一下
    res = math.floor(res * dicimal_n + 0.5) / dicimal_n

    return res .. unit[count]
end

---数字转为加千分位逗号的字符串
---@param num number
---@return nil|string
_M.num2thousandth_str = function(num)
    if type(num) ~= "number" then
        error("util.num2thousandth_str(num) excepted number.")
        return nil, "not number"
    end
    local inter, point = math.modf(num)

    local num_str = tostring(inter)
    local res = ""
    local numLen = string.len(num_str)
    local count = 0
    for i = numLen, 1, -1 do
        if count % 3 == 0 and count ~= 0 then
            res = string.format("%s,%s", string.sub(num_str, i, i), res)
        else
            res = string.format("%s%s", string.sub(num_str, i, i), res)
        end
        count = count + 1
    end

    if point > 0 then
        -- 存在小数点
        local point_str = tostring(point)
        res = res .. string.sub(point_str, 2, string.len(point_str))
    end

    return res
end

---设置table为只读
---@param tbl table
---@return table
_M.table_read_only = function(tbl)
    local temp = tbl or {}
    local mt = {
        __index = function(t, k) return temp[k] end,
        __newindex = function(t, k, v)
            error("attempt to update a read-only table!")
        end
    }
    setmetatable(temp, mt)
    return temp
end

---字符串是否以指定子串开头
---@param str string 待查询字符串
---@param prefix string 是否以此prefix开头
---@return boolean
_M.str_startwith = function(str, prefix)
    return (str:find(prefix, 1, true) == 1)
end

---字符串是否以指定子串结尾
---@param str string 待查询字符串
---@param suffix string 是否以此suffix结尾
---@return boolean
_M.str_endwith = function(str, suffix)
    return (str:sub(-1 * string.len(suffix)) == suffix)
end

---字符串是否包含指定子串
---@param str string 待查询字符串
---@param needle string 是否包含此needle
---@return boolean
_M.str_contains = function(str, needle)
    return (str:find(needle, 1, true) ~= nil)
end

---将短横线连接的字符串(KebabCase)转为下划线连接的字符串(UnderScoreCase)
---@param str string
---@return string
_M.kebabcase_to_underscorecase = function(str)
    return string.gsub(str, "%-", "_")
end

--- 检测table是否为数组  传入变量已经确保是 table
---     是数组返回 true, 非数组(只是table)返回false 
local check_table_is_array = function(tbl)
    local i = 0
    for k in pairs(tbl) do
        i = i + 1
        if tbl[i] == nil then
            return false 
        end
    end
    return true
end

local to_string
---将指定值转为字符串
---     如果是table，递归将所有key value用分隔符连接(数组:用table.concat连接value)
---@param value any 待处理对象
---@param sperator string|nil 如果值是table，连接用的分隔符(默认nil:用空格" "分隔)
---@return string
to_string = function (value, sperator)
    local spec = sperator or " "
    local s
    if type(value) == "table" then
        if(check_table_is_array(value)) then
            -- 是数组
            local tmpArr = {}
            for k, v in ipairs(value) do
                table.insert(tmpArr, to_string(v, spec))
            end

            local ok, res = pcall(table.concat, tmpArr, spec)
            if(ok) then
                return res
            else
                ngx.log(ngx.ERR, ' pcall==table.concat ERR[' .. tostring(res) ..']' ..tostring(ok) )
            end 
        else
            -- 不是数组,只是table
            local tmpArr = {}
            for k, v in pairs(value) do
                table.insert(tmpArr, tostring(k))
                table.insert(tmpArr, to_string(v, spec))
            end
            -- table.concat(tmpArr, spec)
            local ok2, res2 = pcall(table.concat, tmpArr, spec)
            if(ok2) then
                return res2
            else
                ngx.log(ngx.ERR, ' table pairs ==> ERR[' .. tostring(res2) ..']' ..tostring(ok2) )
            end 
        end

        -- table连接成字串出错了
        s = " "
        
    else
        -- 非table
        s = tostring(value)
    end
    return s
end
_M.to_string = to_string

---解析cookie字符串为数组
---     返回数据结构为: {
---         { k = "name1", v="value1"}, { k = "name2", v="value2"}, ...
---     }
---@param cookie_str string
---@return table
_M.cookie_string_to_table = function(cookie_str)
    local origin_str = string_util.trim(cookie_str)
    if origin_str:len() == 0 then
        return {}
    end
    local result = {}
    local arr1 = string_util.split(origin_str, ";")
    for _, kv_str in ipairs(arr1) do
        local from = string.find(kv_str, "=", 1, true)
        if from then
            local k = string_util.trim(string.sub(kv_str, 1, from - 1))
            local v = string_util.trim((string.sub(kv_str, from + 1) or ""))
            table.insert(result, { k = k, v = v })
        end
    end
    return result
end

return _M
